import {generateKeyPair} from "@libp2p/crypto/keys";
import {ChainForkConfig, createBeaconConfig} from "@lodestar/config";
import {ssz} from "@lodestar/types";
import {sleep} from "@lodestar/utils";
import {BeaconChain} from "../../src/chain/chain.js";
import {Eth1ForBlockProductionDisabled} from "../../src/eth1/index.js";
import {ExecutionEngineDisabled} from "../../src/execution/index.js";
import {ArchiveMode} from "../../src/index.js";
import {GossipHandlers, Network, NetworkInitModules, getReqRespHandlers} from "../../src/network/index.js";
import {NetworkOptions, defaultNetworkOptions} from "../../src/network/options.js";
import {GetReqRespHandlerFn} from "../../src/network/reqresp/types.js";
import {getMockedBeaconDb} from "../mocks/mockedBeaconDb.js";
import {createCachedBeaconStateTest} from "./cachedBeaconState.js";
import {ClockStatic} from "./clock.js";
import {testLogger} from "./logger.js";
import {generateState} from "./state.js";

export type NetworkForTestOpts = {
  startSlot?: number;
  opts?: Partial<NetworkOptions>;
  gossipHandlersPartial?: Partial<GossipHandlers>;
  getReqRespHandler?: GetReqRespHandlerFn;
};

export async function getNetworkForTest(
  loggerId: string,
  config: ChainForkConfig,
  opts: NetworkForTestOpts
): Promise<[network: Network, closeAll: () => Promise<void>]> {
  const logger = testLogger(loggerId);
  const startSlot = opts.startSlot ?? 0;

  const block = ssz.phase0.SignedBeaconBlock.defaultValue();
  const state = generateState(
    {
      slot: startSlot,
      finalizedCheckpoint: {
        epoch: 0,
        root: ssz.phase0.BeaconBlock.hashTreeRoot(block.message),
      },
    },
    config
  );

  const beaconConfig = createBeaconConfig(config, state.genesisValidatorsRoot);
  const db = getMockedBeaconDb();
  const privateKey = await generateKeyPair("secp256k1");

  const chain = new BeaconChain(
    {
      safeSlotsToImportOptimistically: 0,
      archiveStateEpochFrequency: 0,
      suggestedFeeRecipient: "",
      blsVerifyAllMainThread: true,
      disableOnBlockError: true,
      disableArchiveOnCheckpoint: true,
      disableLightClientServerOnImportBlockHead: true,
      disablePrepareNextSlot: true,
      minSameMessageSignatureSetsToBatch: 32,
      archiveMode: ArchiveMode.Frequency,
    },
    {
      privateKey,
      config: beaconConfig,
      db,
      dataDir: ".",
      dbName: ".",
      logger,
      processShutdownCallback: () => {},
      // set genesis time so that we are at ALTAIR_FORK_EPOCH
      // mock timer does not work on worker thread
      clock: new ClockStatic(
        startSlot,
        Math.floor(Date.now() / 1000) - startSlot * (beaconConfig.SLOT_DURATION_MS / 1000)
      ),
      metrics: null,
      validatorMonitor: null,
      anchorState: createCachedBeaconStateTest(state, beaconConfig),
      isAnchorStateFinalized: true,
      eth1: new Eth1ForBlockProductionDisabled(),
      executionEngine: new ExecutionEngineDisabled(),
    }
  );

  const modules: Omit<NetworkInitModules, "opts" | "logger"> = {
    config: beaconConfig,
    chain,
    db,
    getReqRespHandler: opts.getReqRespHandler ?? getReqRespHandlers({db, chain}),
    gossipHandlers: opts.gossipHandlersPartial as GossipHandlers,
    privateKey,
    metrics: null,
  };

  const network = await Network.init({
    ...modules,
    opts: {
      ...defaultNetworkOptions,
      maxPeers: 10,
      targetPeers: 1,
      bootMultiaddrs: [],
      localMultiaddrs: ["/ip4/0.0.0.0/tcp/0"],
      discv5FirstQueryDelayMs: 0,
      discv5: null,
      skipParamsLog: true,
      // Disable rate limiting
      rateLimitMultiplier: 0,
      // Increase of following value is just to circumvent the following error in e2e tests
      // > libp2p:mplex rate limit hit when receiving messages
      disconnectThreshold: 255,
      ...opts.opts,
    },
    logger,
  });

  return [
    network,
    async function closeAll() {
      await network.close();
      await chain.close();

      /**
       * We choose random port for the libp2p network. Though our libp2p instance is closed the
       * system still hold the port momentarily. And if next test randomly select the same port
       * it failed with ERR_CONNECTION_REFUSED. To avoid such situation giving a grace period
       * for the system to also cleanup resources.
       */
      await sleep(100);
    },
  ];
}
